1use super::crypto::*;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::encoding::{decode_to_string, encode_string};
7use anyhow::Result;
8use std::collections::HashMap;
9use std::ffi::CString;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14pub struct EscudeBinArchiveBuilder {}
16
17impl EscudeBinArchiveBuilder {
18 pub const fn new() -> Self {
20 EscudeBinArchiveBuilder {}
21 }
22}
23
24impl ScriptBuilder for EscudeBinArchiveBuilder {
25 fn default_encoding(&self) -> Encoding {
26 Encoding::Cp932
27 }
28
29 fn default_archive_encoding(&self) -> Option<Encoding> {
30 Some(Encoding::Cp932)
31 }
32
33 fn build_script(
34 &self,
35 data: Vec<u8>,
36 _filename: &str,
37 _encoding: Encoding,
38 archive_encoding: Encoding,
39 config: &ExtraConfig,
40 _archive: Option<&Box<dyn Script>>,
41 ) -> Result<Box<dyn Script>> {
42 Ok(Box::new(EscudeBinArchive::new(
43 MemReader::new(data),
44 archive_encoding,
45 config,
46 )?))
47 }
48
49 fn build_script_from_file(
50 &self,
51 filename: &str,
52 _encoding: Encoding,
53 archive_encoding: Encoding,
54 config: &ExtraConfig,
55 _archive: Option<&Box<dyn Script>>,
56 ) -> Result<Box<dyn Script>> {
57 if filename == "-" {
58 let data = crate::utils::files::read_file(filename)?;
59 Ok(Box::new(EscudeBinArchive::new(
60 MemReader::new(data),
61 archive_encoding,
62 config,
63 )?))
64 } else {
65 let f = std::fs::File::open(filename)?;
66 let reader = std::io::BufReader::new(f);
67 Ok(Box::new(EscudeBinArchive::new(
68 reader,
69 archive_encoding,
70 config,
71 )?))
72 }
73 }
74
75 fn build_script_from_reader(
76 &self,
77 reader: Box<dyn ReadSeek>,
78 _filename: &str,
79 _encoding: Encoding,
80 archive_encoding: Encoding,
81 config: &ExtraConfig,
82 _archive: Option<&Box<dyn Script>>,
83 ) -> Result<Box<dyn Script>> {
84 Ok(Box::new(EscudeBinArchive::new(
85 reader,
86 archive_encoding,
87 config,
88 )?))
89 }
90
91 fn extensions(&self) -> &'static [&'static str] {
92 &["bin"]
93 }
94
95 fn script_type(&self) -> &'static ScriptType {
96 &ScriptType::EscudeArc
97 }
98
99 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
100 if buf_len > 8 && buf.starts_with(b"ESC-ARC2") {
101 return Some(255);
102 }
103 None
104 }
105
106 fn is_archive(&self) -> bool {
107 true
108 }
109
110 fn create_archive(
111 &self,
112 filename: &str,
113 files: &[&str],
114 encoding: Encoding,
115 config: &ExtraConfig,
116 ) -> Result<Box<dyn Archive>> {
117 let f = std::fs::File::create(filename)?;
118 let writer = std::io::BufWriter::new(f);
119 let archive = EscudeBinArchiveWriter::new(writer, files, encoding, config)?;
120 Ok(Box::new(archive))
121 }
122}
123
124#[derive(Debug)]
125struct BinEntry {
126 name_offset: u32,
127 data_offset: u32,
128 length: u32,
129}
130
131struct Entry {
132 name: String,
133 data: MemReader,
134}
135
136impl Read for Entry {
137 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
138 self.data.read(buf)
139 }
140}
141
142impl ArchiveContent for Entry {
143 fn name(&self) -> &str {
144 &self.name
145 }
146
147 fn script_type(&self) -> Option<&ScriptType> {
148 if self.data.data.starts_with(b"ESCR1_00") {
149 Some(&ScriptType::Escude)
150 } else if self.data.data.starts_with(b"LIST") {
151 Some(&ScriptType::EscudeList)
152 } else {
153 None
154 }
155 }
156
157 fn data(&mut self) -> Result<Vec<u8>> {
158 Ok(self.data.data.clone())
159 }
160
161 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
162 Ok(Box::new(&mut self.data))
163 }
164}
165
166#[derive(Debug)]
167pub struct EscudeBinArchive<T: Read + Seek + std::fmt::Debug> {
169 reader: Arc<Mutex<T>>,
170 file_count: u32,
171 entries: Vec<BinEntry>,
172 archive_encoding: Encoding,
173}
174
175impl<T: Read + Seek + std::fmt::Debug> EscudeBinArchive<T> {
176 pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
182 let mut header = [0u8; 8];
183 reader.read_exact(&mut header)?;
184 if &header != b"ESC-ARC2" {
185 return Err(anyhow::anyhow!("Invalid Escude binary script header"));
186 }
187 reader.seek(SeekFrom::Start(0xC))?;
188 let mut crypto_reader = CryptoReader::new(&mut reader)?;
189 let file_count = crypto_reader.read_u32()?;
190 let _name_tbl_len = crypto_reader.read_u32()?;
191 let mut entries = Vec::with_capacity(file_count as usize);
192 for _ in 0..file_count {
193 let name_offset = crypto_reader.read_u32()?;
194 let data_offset = crypto_reader.read_u32()?;
195 let length = crypto_reader.read_u32()?;
196 entries.push(BinEntry {
197 name_offset,
198 data_offset,
199 length,
200 });
201 }
202 Ok(EscudeBinArchive {
203 reader: Arc::new(Mutex::new(reader)),
204 file_count,
205 entries,
206 archive_encoding,
207 })
208 }
209}
210
211impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for EscudeBinArchive<T> {
212 fn default_output_script_type(&self) -> OutputScriptType {
213 OutputScriptType::Json
214 }
215
216 fn default_format_type(&self) -> FormatOptions {
217 FormatOptions::None
218 }
219
220 fn is_archive(&self) -> bool {
221 true
222 }
223
224 fn iter_archive_filename<'a>(
225 &'a self,
226 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
227 Ok(Box::new(EscudeBinArchiveIter {
228 entries: self.entries.iter(),
229 reader: self.reader.clone(),
230 file_count: self.file_count,
231 archive_encoding: self.archive_encoding,
232 }))
233 }
234
235 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
236 Ok(Box::new(
237 self.entries.iter().map(|e| Ok(e.data_offset as u64)),
238 ))
239 }
240
241 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
242 if index >= self.entries.len() {
243 return Err(anyhow::anyhow!(
244 "Index out of bounds: {} (max: {})",
245 index,
246 self.entries.len()
247 ));
248 }
249 let entry = &self.entries[index];
250 let name = self
251 .reader
252 .cpeek_cstring_at(entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14)?;
253 let name = decode_to_string(self.archive_encoding, name.as_bytes(), true)?;
254 let mut data = self
255 .reader
256 .cpeek_at_vec(entry.data_offset as u64, entry.length as usize)?;
257 if data.starts_with(b"acp") {
258 let mut decoder = match super::lzw::LZWDecoder::new(&data) {
259 Ok(decoder) => decoder,
260 Err(e) => return Err(anyhow::anyhow!("Failed to create LZW decoder: {}", e)),
261 };
262 data = decoder.unpack()?;
263 }
264 Ok(Box::new(Entry {
265 name,
266 data: MemReader::new(data),
267 }))
268 }
269}
270
271struct EscudeBinArchiveIter<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
272 entries: T,
273 reader: Arc<Mutex<R>>,
274 file_count: u32,
275 archive_encoding: Encoding,
276}
277
278impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
279 for EscudeBinArchiveIter<'a, T, R>
280{
281 type Item = Result<String>;
282
283 fn next(&mut self) -> Option<Self::Item> {
284 let entry = match self.entries.next() {
285 Some(entry) => entry,
286 None => return None,
287 };
288 let name_offset = entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14;
289 let name = match self.reader.cpeek_cstring_at(name_offset) {
290 Ok(name) => name,
291 Err(e) => return Some(Err(e.into())),
292 };
293 let name = match decode_to_string(self.archive_encoding, name.as_bytes(), true) {
294 Ok(name) => name,
295 Err(e) => return Some(Err(e.into())),
296 };
297 Some(Ok(name))
298 }
299}
300
301pub struct EscudeBinArchiveWriter<T: Write + Seek> {
303 writer: T,
304 headers: HashMap<String, BinEntry>,
305 name_tbl_len: u32,
306 fake: bool,
307}
308
309impl<T: Write + Seek> EscudeBinArchiveWriter<T> {
310 pub fn new(
317 mut writer: T,
318 files: &[&str],
319 encoding: Encoding,
320 config: &ExtraConfig,
321 ) -> Result<Self> {
322 writer.write_all(b"ESC-ARC2")?;
323 let header_len = 0xC + 0xC * files.len();
324 let header = vec![0u8; header_len];
325 writer.write_all(&header)?;
326 let mut headers = HashMap::new();
327 for file in files {
328 let f = file.to_string();
329 let encoded = encode_string(encoding, file, false)?;
330 let encoded = CString::new(encoded)?;
331 let name_offset = writer.stream_position()? as u32;
332 writer.write_all(encoded.as_bytes_with_nul())?;
333 headers.insert(
334 f,
335 BinEntry {
336 name_offset,
337 data_offset: 0,
338 length: 0,
339 },
340 );
341 }
342 let name_tbl_len = writer.stream_position()? as u32 - header_len as u32 - 0x8;
343 Ok(EscudeBinArchiveWriter {
344 writer,
345 headers,
346 name_tbl_len,
347 fake: config.escude_fake_compress,
348 })
349 }
350}
351
352impl<T: Write + Seek> Archive for EscudeBinArchiveWriter<T> {
353 fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
354 let entry = self
355 .headers
356 .get_mut(name)
357 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
358 if entry.data_offset != 0 {
359 return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
360 }
361 entry.data_offset = self.writer.stream_position()? as u32;
362 Ok(Box::new(EscudeBinArchiveFileWithLzw::new(
363 entry,
364 &mut self.writer,
365 self.fake,
366 )?))
367 }
368
369 fn write_header(&mut self) -> Result<()> {
370 self.writer.seek(SeekFrom::Start(0x8))?;
371 let mut crypto = CryptoWriter::new(&mut self.writer)?;
372 let file_count = self.headers.len() as u32;
373 crypto.write_u32(file_count)?;
374 crypto.write_u32(self.name_tbl_len)?;
375 let mut entries: Vec<_> = self.headers.values().collect();
376 entries.sort_by(|a, b| a.name_offset.cmp(&b.name_offset));
377 for entry in entries {
378 let name_offset = entry.name_offset - file_count * 12 - 0x14;
379 crypto.write_u32(name_offset)?;
380 crypto.write_u32(entry.data_offset)?;
381 crypto.write_u32(entry.length)?;
382 }
383 Ok(())
384 }
385}
386
387pub struct EscudeBinArchiveFileWithLzw<'a, T: Write + Seek> {
389 writer: EscudeBinArchiveFile<'a, T>,
390 buf: MemWriter,
391 fake: bool,
392}
393
394impl<'a, T: Write + Seek> EscudeBinArchiveFileWithLzw<'a, T> {
395 fn new(header: &'a mut BinEntry, writer: &'a mut T, fake: bool) -> Result<Self> {
396 let writer = EscudeBinArchiveFile {
397 header,
398 writer,
399 pos: 0,
400 };
401 Ok(EscudeBinArchiveFileWithLzw {
402 writer,
403 buf: MemWriter::new(),
404 fake,
405 })
406 }
407}
408
409impl<'a, T: Write + Seek> Write for EscudeBinArchiveFileWithLzw<'a, T> {
410 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
411 self.buf.write(buf)
412 }
413
414 fn flush(&mut self) -> std::io::Result<()> {
415 self.buf.flush()
416 }
417}
418
419impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFileWithLzw<'a, T> {
420 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
421 self.buf.seek(pos)
422 }
423
424 fn stream_position(&mut self) -> std::io::Result<u64> {
425 self.buf.stream_position()
426 }
427
428 fn rewind(&mut self) -> std::io::Result<()> {
429 self.buf.rewind()
430 }
431}
432
433impl<'a, T: Write + Seek> Drop for EscudeBinArchiveFileWithLzw<'a, T> {
434 fn drop(&mut self) {
435 let buf = self.buf.as_slice();
436 let encoder = super::lzw::LZWEncoder::new();
437 let data = match encoder.encode(buf, self.fake) {
438 Ok(data) => data,
439 Err(e) => {
440 eprintln!("Failed to encode LZW data: {}", e);
441 crate::COUNTER.inc_error();
442 return;
443 }
444 };
445 match self.writer.write_all(&data) {
446 Ok(_) => {}
447 Err(e) => {
448 eprintln!("Failed to write LZW data: {}", e);
449 crate::COUNTER.inc_error();
450 }
451 }
452 }
453}
454
455pub struct EscudeBinArchiveFile<'a, T: Write + Seek> {
457 header: &'a mut BinEntry,
458 writer: &'a mut T,
459 pos: usize,
460}
461
462impl<'a, T: Write + Seek> Write for EscudeBinArchiveFile<'a, T> {
463 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
464 self.writer.seek(SeekFrom::Start(
465 self.header.data_offset as u64 + self.pos as u64,
466 ))?;
467 let written = self.writer.write(buf)?;
468 self.pos += written;
469 self.header.length = self.header.length.max(self.pos as u32);
470 Ok(written)
471 }
472
473 fn flush(&mut self) -> std::io::Result<()> {
474 self.writer.flush()
475 }
476}
477
478impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFile<'a, T> {
479 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
480 let new_pos = match pos {
481 SeekFrom::Start(offset) => offset as usize,
482 SeekFrom::End(offset) => {
483 if offset < 0 {
484 if (-offset) as usize > self.header.length as usize {
485 return Err(std::io::Error::new(
486 std::io::ErrorKind::InvalidInput,
487 "Seek from end exceeds file length",
488 ));
489 }
490 self.header.length as usize - (-offset) as usize
491 } else {
492 self.header.length as usize + offset as usize
493 }
494 }
495 SeekFrom::Current(offset) => {
496 if offset < 0 {
497 if (-offset) as usize > self.pos {
498 return Err(std::io::Error::new(
499 std::io::ErrorKind::InvalidInput,
500 "Seek from current exceeds current position",
501 ));
502 }
503 self.pos.saturating_sub((-offset) as usize)
504 } else {
505 self.pos + offset as usize
506 }
507 }
508 };
509 self.pos = new_pos;
510 Ok(self.pos as u64)
511 }
512}